package cn.itcast.redis;

import cn.itcast.SpringBeanUtil;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.BitSet;

/**
 * Redis咆哮位图的练习
 * create by zhangtian3 on 2019/7/1
 */
@Component
public class UserBitMapRedis {
    //由于当前class不在spring boot框架内（不在web项目中）所以无法使用autowired，使用此种方法进行注入
    private static RedisTemplate<String, String> template = (RedisTemplate<String, String>) SpringBeanUtil.getBean("redisTemplate");
    private static final String USER_ID_SEQ = "user_id_seq" ;

    /**
     * 用户ID向数字的映射
     * @param userId
     * @return
     */
    public Boolean userIdToLong(String userId) {
        boolean result = template.execute(new RedisCallback<Boolean>() {
            @Override
            public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
                RedisSerializer<String> serializer = template.getStringSerializer();
                if (connection.exists(serializer.serialize(userId))) {
                    return false ;
                }

                Long userSeq = connection.incr(serializer.serialize(USER_ID_SEQ)) ;
                return connection.set(serializer.serialize(userId),  serializer.serialize(String.valueOf(userSeq)));
            }
        });

        return result ;
    }

    // ------------------------------- 日活跃用户 ------------------------------
    private static final String ACTIVE_USER = "activeUser:" ;

    // 保存活跃用户数据 以id为偏移量，1为用户今天已登录，0为用户今天未登陆。
    public void setActiveUserCount(String userId) {
        boolean result = template.execute(new RedisCallback<Boolean>() {
            @Override
            public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
                RedisSerializer<String> serializer = template.getStringSerializer();
                byte[] value = connection.get(serializer.serialize(userId));
                if (value != null && value.length > 0) {
                    DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd") ;
                    Long id = Long.valueOf(serializer.deserialize(value)) ;
                    String key = ACTIVE_USER + LocalDate.now().format(dtf) ;
                    connection.setBit(serializer.serialize(key), id, true) ;
                }

                return true ;
            }
        });
    }

    /**
     * 获取用户当天登录状态
     * @param userId
     */
    public Boolean getCurrentDateActiveStatus(String userId){
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd") ;
        Boolean result = template.execute(new RedisCallback<Boolean>() {
            @Override
            public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
                Boolean isLogin = false ;
                RedisSerializer<String> serializer = template.getStringSerializer();
                byte[] value = connection.get(serializer.serialize(userId));
                if (value != null && value.length > 0) {
                    Long id = Long.valueOf(serializer.deserialize(value)) ;
                    LocalDate localDate = LocalDate.now() ;
                    // 获得当天的日期
                    String key = ACTIVE_USER + localDate.format(dtf) ;
                    // 获得这一天日期的活跃用户字节数组
                    isLogin = connection.getBit(serializer.serialize(key), id) ;
                }

                return isLogin ;
            }
        });

        return result ;
    }

    /**
     * 统计当天登录用户数
     * @param userId
     * @return
     */
    public Long countLoginDays(String userId){
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd") ;
        Long result = template.execute(new RedisCallback<Long>() {
            @Override
            public Long doInRedis(RedisConnection connection) throws DataAccessException {
                Long count = 0L ;
                RedisSerializer<String> serializer = template.getStringSerializer();
                byte[] value = connection.get(serializer.serialize(userId));
                if (value != null && value.length > 0) {
                    LocalDate localDate = LocalDate.now() ;
                    // 获得当天的日期
                    String key = ACTIVE_USER + localDate.format(dtf) ;
                    // 获得这一天日期的活跃用户字节数组
                    count = connection.bitCount(serializer.serialize(key)) ;// key统计用户数，当天登录的用户数，日活
                }

                return count ;
            }
        });

        return result ;
    }

    // 统计七天内有进行至少一次登录操作的用户人数
    // 采取按位存储的方式，bit的下标为用户的id，如果用户id的下标value为1，那么该用户在当天有进行登录
    // 将所用的用户生成一个位图（0/1 未登录/登录）
    // 统计七天内，有过一次登录的用户，那么他就是活跃用户，所以是获取七天的字节数组，取他们的并集，用or。如果想获取七天都有登录的用户，那么需要的是取他们的交集，用and
    public Integer getActiveUserCount() {
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd") ;
        BitSet all = new BitSet() ;
        Integer result = template.execute(new RedisCallback<Integer>() {
            @Override
            public Integer doInRedis(RedisConnection connection) throws DataAccessException {
                RedisSerializer<String> serializer = template.getStringSerializer();
                LocalDate localDate = LocalDate.now() ;
                for (int i = 0; i < 7; i++) {
                    // 获得前一天的日期
                    String key = ACTIVE_USER + localDate.format(dtf) ;

                    // 获得这一天日期的活跃用户字节数组
                    byte[] loginByte = connection.get(serializer.serialize(key)) ;
                    // 有可能当天没有一个用户登录  7天只要有一天登录
                    if(loginByte != null){
                        BitSet user = BitSet.valueOf(loginByte);
                        // 取其并集
                        all.or(user);
                    }

                    // 7天全部登录校验
                    /*if (loginByte != null) {
                        BitSet user = BitSet.valueOf(loginByte);
                        if (all.isEmpty()) {
                            all.or(user);
                        } else {
                            all.and(user);
                        }
                    }else {
                        all.clear();
                        break;
                    }*/

                    localDate = localDate.minusDays(1) ;
                }

                // 统计人数
                Integer count = all.cardinality() ;
                return count ;
            }
        });

        return result ;
    }

    // --------------------- Bit运算 -----------------------------
    // 来一个用户，就将相应位图中相应的位置为一。如果位从0变成1，那么就可以给阅读数加1。这样就可以很方便的获得文章的阅读数
    // 可以动态计算阅读了两篇文章的公共用户量有多少？将两个位图做一下 AND 计算，然后统计位图中位 1 的个数。同样，还可以有 OR 计算、XOR 计算等等都是可行
    // Redis 的位图是密集位图，什么意思呢？
    // 如果有一个很大的位图，它只有最后一个位是 1，其它都是零，这个位图还是会占用全部的内存空间，这就不是一般的浪费了。
    // 你可以想象大部分文章的阅读量都不大，但是它们的占用空间却是很接近的，和哪些爆款文章占据的内存差不多
    // 这时咆哮位图（RoaringBitmap）来了
}
